package com.hts.idempontent;

import io.netty.util.HashedWheelTimer;
import io.netty.util.Timeout;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

/**
 * 基于redis 做幂等
 * @author 红塔山
 */
@Component
@Slf4j
public class RedisIdempotent implements Idempotent {
    /**
     * redis template
     */
    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    /**
     * 定时任务轮 （也可以变成 定时任务线程池）
     */
    private final HashedWheelTimer hashedWheelTimer;
    /**
     * 默认时间
     */
    private final Duration defaultIdempotent;

    ThreadLocal<String> names = new ThreadLocal<>();

    public Map<String, WatchDog> timeoutMap;

    public RedisIdempotent() {
        //30s
        this.defaultIdempotent = Duration.ofSeconds(30);
        //定时任务  用于延时任务
        hashedWheelTimer = new HashedWheelTimer();
        //多线程
        timeoutMap = new ConcurrentHashMap<>();
    }

    @Override
    public Boolean getIdempotent(String key, Duration idempotent) {
        //如果当前的时间为-1
        String currentName = getCurrentName();
        //是自动续时间
        if (idempotent.compareTo(Duration.ofSeconds(-1L)) == 0) {
            idempotent = defaultIdempotent;
            //获取幂等
            Boolean result = tryIdempotentSync(List.of(key), (int) idempotent.getSeconds(), currentName);
            //设置成功之后延续时间
            if (result) {
                WatchDog watchDog = new WatchDog();
                watchDog.setCurrentName(getCurrentName());
                timeoutMap.put(currentName, watchDog);
                watchDog(key, currentName);
            }

            return result;

        }
        return tryIdempotentSync(List.of(key), (int) idempotent.getSeconds(), currentName);
    }

    /**
     * 获取当前幂等的名称
     *
     * @return
     */
    public String getCurrentName() {
        String s = names.get();
        if (s == null || s.isEmpty()) {
            long id = Thread.currentThread().getId();
            String uuid = UUID.randomUUID().toString().replace("_", "");
            String name = uuid + ":" + id;
            names.set(name);
        }
        return names.get();

    }

    /**
     * 测试 是否可以获取幂等
     *
     * @param keys
     * @param avg
     * @return
     */
    public Boolean tryIdempotentSync(List<String> keys, Object... avg) {
        DefaultRedisScript<Boolean> defaultRedisScript = getBooleanDefaultRedisScript();
        return redisTemplate.execute(defaultRedisScript, keys, avg);
    }

    private DefaultRedisScript<Boolean> getBooleanDefaultRedisScript() {
        String tryIdempotent = """
                if (redis.call('exists', KEYS[1]) == 0) then
                    redis.call('hset', KEYS[1], ARGV[2], 1);
                    redis.call('expire', KEYS[1], ARGV[1]);
                    return true; 
                end;
                return false;    
                """;

        DefaultRedisScript<Boolean> defaultRedisScript = new DefaultRedisScript<>();
        defaultRedisScript.setScriptText(tryIdempotent);
        defaultRedisScript.setResultType(Boolean.class);
        return defaultRedisScript;
    }

    /**
     * 自动续时间
     *
     * @param key
     * @param currentName
     */
    public void watchDog(String key, String currentName) {
        WatchDog watchDog = timeoutMap.get(currentName);
        if (watchDog == null) {
            return;
        }
        Timeout timeout = hashedWheelTimer.newTimeout(timeoutParam -> {
            log.info("开始续时间");
            WatchDog watchDogThread = timeoutMap.get(currentName);
            if (watchDogThread == null) {
                return;
            }
            Boolean result = renewExpiration(List.of(key), (int) defaultIdempotent.getSeconds(), currentName);
            log.info("续时间结果：{}", result);
            //再次延迟
            if (result) {
                watchDog(key, currentName);
            }
        }, defaultIdempotent.getSeconds() / 3, TimeUnit.SECONDS);
        watchDog.setTimeout(timeout);
    }

    /**
     * @param keys
     * @param avg
     * @return
     */
    public Boolean renewExpiration(List<String> keys, Object... avg) {
        String renewExpiration = "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                "redis.call('expire', KEYS[1], ARGV[1]); " +
                "return true; " +
                "end; " +
                "return false;";

        DefaultRedisScript<Boolean> defaultRedisScript = new DefaultRedisScript<>();
        defaultRedisScript.setScriptText(renewExpiration);
        defaultRedisScript.setResultType(Boolean.class);
        return redisTemplate.execute(defaultRedisScript, keys, avg);
    }


    @Override
    public Boolean cleanIdempotent(String key) {
        //清除缓存
        Boolean delete = redisTemplate.delete(key);
        String currentName = getCurrentName();
        WatchDog watchDog = timeoutMap.get(currentName);

        if (Objects.nonNull(watchDog)) {
            Timeout timeout = watchDog.getTimeout();
            if (Objects.nonNull(timeout)) {
                log.info("停止看门狗");
                timeout.cancel();
            }
            //清除数据
            timeoutMap.remove(currentName);
        }
        names.remove();
        return delete;
    }

    @Data
    public static class WatchDog {
        /**
         * 延时任务
         */
        private Timeout timeout;
        /**
         * 当前时间
         */
        private String currentName;

        public void cancel() {
            if (this.timeout != null) {
                timeout.cancel();
            }
        }
    }


}
